"""Support for bemfa service."""
from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from collections.abc import Mapping, Callable
import hashlib
from typing import Any
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.util.decorator import Registry
from homeassistant.util.read_only_dict import ReadOnlyDict

from .const import MSG_OFF, MSG_SEPARATOR, OPTIONS_NAME, TOPIC_PREFIX, TopicSuffix

_LOGGING = logging.getLogger(__name__)


class Sync(ABC):
    """An abstract class for bemfa syncs."""

    @staticmethod
    @abstractmethod
    def get_config_step_id() -> str:
        """StepId for options flow while config this kind of sync."""
        raise NotImplementedError

    @staticmethod
    @abstractmethod
    def _get_topic_suffix() -> TopicSuffix:
        raise NotImplementedError

    @classmethod
    @abstractmethod
    def collect_supported_syncs(cls, hass: HomeAssistant):
        """Collect all supported bemfa syncs from hass."""
        raise NotImplementedError

    def __init__(
        self,
        hass: HomeAssistant,
        entity_id: str,
        name: str,
    ) -> None:
        """Initialize."""
        self._hass = hass
        self._entity_id = entity_id
        self._name = name
        self._topic = None
        self._config = {}

    @property
    def entity_id(self) -> str:
        """Entity id in hass."""
        return self._entity_id

    @property
    def name(self) -> str:
        """Name in bemfa service."""
        return self._name

    @name.setter
    def name(self, name: str):
        self._name = name

    @property
    def config(self) -> dict[str, str]:
        """User config stored in integration options."""
        return self._config

    @config.setter
    def config(self, config: dict[str, str]):
        self._config = config

    @property
    def topic(self) -> str:
        """Get bemfa topic generated by hass entity id.
        Bemfa service commuicates with devices by mqtt.
        Each device corresponds to a particular topic whose suffix is a 3 digit number to indicate its type.
        """
        if self._topic is None:
            # Bemfa topic supports alphanumeric only, md5 generates unique alphanumeric string of each entity id regardless of its format.
            self._topic = (
                TOPIC_PREFIX
                + hashlib.md5(self._entity_id.encode("utf-8")).hexdigest()
                + self._get_topic_suffix()
            )
        return self._topic

    def generate_option_label(self) -> str:
        """Generate label in front end options list as "[domain]name"."""
        domain = self._entity_id.split(".")[0]
        return "[{domain}] {name}".format(domain=domain, name=self._name)

    def generate_details_schema(self) -> dict[str, Any]:
        """Generate schema in front end details setting form."""
        return {vol.Required(OPTIONS_NAME, default=self._name): str}

    @abstractmethod
    def get_watched_entity_ids(self) -> list[str]:
        """When state of one of these entites changed, send mqtt msg to bemfa servcie."""
        raise NotImplementedError

    def generate_msg(self) -> str:
        """Generate mqtt msg to send to bemfa service."""
        parts = self._generate_msg_parts()

        # remove useless tail parts
        while len(parts) > 0 and parts[len(parts) - 1] == "":
            parts.pop()

        return MSG_SEPARATOR.join(parts)

    @abstractmethod
    def _generate_msg_parts(self) -> list[str]:
        raise NotImplementedError


SYNC_TYPES: Registry[str, type[Sync]] = Registry()


class ControllableSync(Sync):
    """An abstract class for controllable bemfa sync."""

    @staticmethod
    @abstractmethod
    def _supported_domain() -> str | list[str]:
        """Hass domain(s) from which we collect syncs."""
        raise NotImplementedError

    @classmethod
    def collect_supported_syncs(cls, hass: HomeAssistant):
        return [
            cls(hass, state.entity_id, state.name)
            for state in hass.states.async_all(cls._supported_domain())
        ]

    def get_watched_entity_ids(self) -> list[str]:
        return [self._entity_id]

    def _generate_msg_parts(self) -> list[str]:
        state = self._hass.states.get(self._entity_id)
        if state is None:
            return []

        generators = self._msg_generators()
        msg = [generators[0](state.state, state.attributes)]

        # if first one is off, the following parts is useless
        if msg[0] != MSG_OFF:
            msg += list(
                map(
                    lambda f: str(f(state.state, state.attributes)),
                    generators[1:],
                )
            )
        return msg

    @abstractmethod
    def _msg_generators(
        self,
    ) -> list[Callable[[str, ReadOnlyDict[Mapping[str, Any]]], str | int]]:
        raise NotImplementedError

    def resolve_msg(self, msg: str):
        """Resolve mqtt msg received from bemfa service."""
        state = self._hass.states.get(self._entity_id)
        if state is None:
            return

        msg_list: list[str] = msg.split(MSG_SEPARATOR)
        if msg_list[0] == MSG_OFF:
            msg_list = [MSG_OFF]  # discard any data followed by "off"

        # generate msg from entity to compare to received msg
        state_msg_list = MSG_SEPARATOR.join(self._generate_msg_parts()).split(
            MSG_SEPARATOR
        )

        for resolver in self._msg_resolvers():
            start_index = resolver[0]
            end_index = min(resolver[1], len(msg_list), len(state_msg_list))
            if msg_list[start_index:end_index] != state_msg_list[start_index:end_index]:
                (domain, service, data) = resolver[2](
                    [
                        int(msg) if msg.isdigit() else msg
                        for msg in msg_list[start_index:end_index]
                    ],
                    state.attributes,
                )
                data.update({ATTR_ENTITY_ID: self._entity_id})
                self._hass.services.call(
                    domain=domain, service=service, service_data=data
                )
                break  # call only one service at most on each msg received

    @abstractmethod
    def _msg_resolvers(
        self,
    ) -> list[
        (
            int,
            int,
            Callable[
                [list[str | int], ReadOnlyDict[Mapping[str, Any]]],
                (str, str, dict[str, Any]),
            ],
        )
    ]:
        raise NotImplementedError
